﻿using System.Web.Security;
using System.Configuration.Provider;
using System.Collections.Specialized;
using System;
using System.Data;
using System.Data.Odbc;
using System.Configuration;
using System.Diagnostics;
using System.Web;
using System.Globalization;
using MySql.Data.MySqlClient;
using System.Collections.Generic;
using Reviewer.Security.Data;

namespace Reviewer.Security
{
    /// <summary>
    /// Reviewer role provider
    /// </summary>
    public class RoleProvider : System.Web.Security.RoleProvider
    {
        #region methods (static)
        private static void WriteToEventLog(MySqlException e, string action)
        {
            //TODO: Add logging functionality
            System.Diagnostics.Trace.TraceError("RoleProvider/{0} caused an error: {1}", action, e.ToString());
            return;
        }
        #endregion

        #region fields (private)
        private string exceptionMessage = "An exception occurred.";
        private ConnectionStringSettings connectionStringSettings;
        private string connectionString;
        #endregion

        #region properties (public)
        /// <summary>
        /// Gets or sets a value indicating whether if exceptions should be written to the event log].
        /// </summary>
        /// <value>
        /// 	<c>true</c> if [write exceptions to event log]; otherwise, <c>false</c>.
        /// </value>
        public bool WriteExceptionsToEventLog
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the name of the application to store and retrieve role information for.
        /// </summary>
        /// <value></value>
        /// <returns>
        /// The name of the application to store and retrieve role information for.
        /// </returns>
        public override string ApplicationName
        {
            get;
            set;
        }
        #endregion

        #region methods (public)
        /// <summary>
        /// Initializes the provider.
        /// </summary>
        /// <param name="name">The friendly name of the provider.</param>
        /// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param>
        /// <exception cref="T:System.ArgumentNullException">
        /// The name of the provider is null.
        /// </exception>
        /// <exception cref="T:System.ArgumentException">
        /// The name of the provider has a length of zero.
        /// </exception>
        /// <exception cref="T:System.InvalidOperationException">
        /// An attempt is made to call <see cref="M:System.Configuration.Provider.ProviderBase.Initialize(System.String,System.Collections.Specialized.NameValueCollection)"/> on a provider after the provider has already been initialized.
        /// </exception>
        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            if (String.IsNullOrEmpty(name))
            {
                name = "ReviewerRoleProvider";
            }

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Reviewer role provider");
            }

            base.Initialize(name, config);

            if (String.IsNullOrEmpty(config["applicationName"]) || String.IsNullOrEmpty(config["applicationName"].Trim()))
            {
                ApplicationName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;
            }
            else
            {
                ApplicationName = config["applicationName"];
            }

            if (config["writeExceptionsToEventLog"] != null)
            {
                if (config["writeExceptionsToEventLog"].ToUpper(CultureInfo.InvariantCulture).Equals("TRUE"))
                {
                    WriteExceptionsToEventLog = true;
                }
            }

            connectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]];

            if (connectionStringSettings == null || String.IsNullOrEmpty(connectionStringSettings.ConnectionString) || String.IsNullOrEmpty(connectionStringSettings.ConnectionString.Trim()))
            {
                throw new ProviderException("Connection string cannot be blank.");
            }

            connectionString = connectionStringSettings.ConnectionString;
        }

        /// <summary>
        /// Adds the users to roles.
        /// </summary>
        /// <param name="usernames">A string array of user names to be added to the specified roles.</param>
        /// <param name="roleNames">The role names.</param>
        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            foreach (string rolename in roleNames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("Role name not found.");
                }
            }

            foreach (string username in usernames)
            {
                if (username.Contains(","))
                {
                    throw new ArgumentException("User names cannot contain commas.");
                }

                foreach (string rolename in roleNames)
                {
                    if (IsUserInRole(username, rolename))
                    {
                        throw new ProviderException("User is already in role.");
                    }
                }
            }

            using (MySqlCommand cmd = new MySqlCommand(
                /* INSERT INTO MembersInRoles (Username, Rolename, ApplicationName) Values(?, ?, ?) */
                String.Format(CultureInfo.InvariantCulture, "INSERT INTO {0} ({1}, {2}, {3}) VALUES (@{1}, @{2}, @{3})"
                , MembersInRolesTable.TABLE_NAME
                , MembersInRolesTable.Username.Name
                , MembersInRolesTable.RoleName.Name
                , MembersInRolesTable.ApplicationName.Name)))
            {
                MembersInRolesTable.ApplicationName.AddParameter(cmd, ApplicationName);
                MySqlParameter userParam = MembersInRolesTable.Username.AddParameter(cmd, String.Empty);
                MySqlParameter roleParam = MembersInRolesTable.RoleName.AddParameter(cmd, String.Empty);

                foreach (string username in usernames)
                {
                    foreach (string rolename in roleNames)
                    {
                        userParam.Value = username;
                        roleParam.Value = rolename;
                        ExecuteNonQuery(cmd, "AddUsersToRoles");
                    }
                }
            }
        }

        /// <summary>
        /// Creates the role.
        /// </summary>
        /// <param name="roleName">The name of the role to create.</param>
        public override void CreateRole(string roleName)
        {
            if (roleName.Contains(","))
            {
                throw new ArgumentException("Role names cannot contain commas.");
            }

            if (RoleExists(roleName))
            {
                throw new ProviderException("Role name already exists.");
            }

            using (MySqlCommand cmd = new MySqlCommand(
                /* INSERT INTO MembershipRole (Rolename, ApplicationName) Values(?, ?) */
                String.Format(CultureInfo.InvariantCulture, "INSERT INTO {0} ({1}, {2}) Values(@{1}, @{2})"
                , MembershipRoleTable.TABLE_NAME
                , MembershipRoleTable.RoleName.Name
                , MembershipRoleTable.ApplicationName.Name)))
            {
                MembershipRoleTable.RoleName.AddParameter(cmd, roleName);
                MembershipRoleTable.ApplicationName.AddParameter(cmd, ApplicationName);

                ExecuteNonQuery(cmd, "CreateRole");
            }
        }

        /// <summary>
        /// Deletes the role.
        /// </summary>
        /// <param name="roleName">The name of the role to delete.</param>
        /// <param name="throwOnPopulatedRole">if set to <c>true</c> [throw on populated role].</param>
        /// <returns>
        /// true if the role was successfully deleted; otherwise, false.
        /// </returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            if (!RoleExists(roleName))
            {
                throw new ProviderException("Role does not exist.");
            }

            if (throwOnPopulatedRole && GetUsersInRole(roleName).Length > 0)
            {
                throw new ProviderException("Cannot delete a populated role.");
            }

            try
            {
                using (MySqlCommand cmd = new MySqlCommand(
                    /* DELETE FROM MembershipRole WHERE Rolename = ? AND ApplicationName = ? */
                    String.Format(CultureInfo.InvariantCulture, "DELETE FROM {0} WHERE {1} = @{1} AND {2} = @{2}"
                    , MembershipRoleTable.TABLE_NAME
                    , MembershipRoleTable.RoleName.Name
                    , MembershipRoleTable.ApplicationName.Name)))
                {
                    MembershipRoleTable.RoleName.AddParameter(cmd, roleName);
                    MembershipRoleTable.ApplicationName.AddParameter(cmd, ApplicationName);

                    using (MySqlCommand cmd2 = new MySqlCommand(
                        /* DELETE FROM MembersInRoles WHERE Rolename = ? AND ApplicationName = ? */
                        String.Format(CultureInfo.InvariantCulture, "DELETE FROM {0} WHERE {1} = @{1} AND {2} = @{2}"
                        , MembershipRoleTable.TABLE_NAME
                        , MembershipRoleTable.RoleName.Name
                        , MembershipRoleTable.ApplicationName.Name)))
                    {
                        MembershipRoleTable.RoleName.AddParameter(cmd2, roleName);
                        MembershipRoleTable.ApplicationName.AddParameter(cmd2, ApplicationName);

                        ExecuteNonQuery(cmd2, "DeleteRole");
                    }

                    ExecuteNonQuery(cmd, "DeleteRole");
                }
            }
            catch
            {
                //Swallow exception
                return false;
            }
            return true;
        }

        /// <summary>
        /// Gets a list of all the roles for the configured applicationName.
        /// </summary>
        /// <returns>
        /// A string array containing the names of all the roles stored in the data source for the configured applicationName.
        /// </returns>
        public override string[] GetAllRoles()
        {
            List<String> roles = new List<String>();

            using (MySqlCommand cmd = new MySqlCommand(
                /* SELECT Rolename FROM MembershipRole WHERE ApplicationName = ? */
                String.Format(CultureInfo.InvariantCulture, "SELECT {1} FROM {0} WHERE {2} = @{2}"
                , MembershipRoleTable.TABLE_NAME
                , MembershipRoleTable.RoleName.Name
                , MembershipRoleTable.ApplicationName.Name)))
            {
                MembershipRoleTable.ApplicationName.AddParameter(cmd, ApplicationName);

                using (IDataReader reader = ExecuteReader(cmd, "GetAllRoles"))
                {
                    while (reader.Read())
                    {
                        roles.Add(reader.GetString(reader.GetOrdinal(MembershipRoleTable.RoleName.Name)));
                    }
                }
            }

            return roles.ToArray();
        }

        /// <summary>
        /// Gets a list of the roles that a specified user is in for the configured applicationName.
        /// </summary>
        /// <param name="username">The user to return a list of roles for.</param>
        /// <returns>
        /// A string array containing the names of all the roles that the specified user is in for the configured applicationName.
        /// </returns>
        public override string[] GetRolesForUser(string username)
        {
            List<String> rolesForUser = new List<string>();

            using (MySqlCommand cmd = new MySqlCommand(
                /* SELECT Rolename FROM MembersInRoles WHERE Username = ? AND ApplicationName = ? */
                String.Format(CultureInfo.InvariantCulture, "SELECT {3} FROM {0} WHERE {1} = @{1} AND {2} = @{2}"
                , MembersInRolesTable.TABLE_NAME
                , MembersInRolesTable.Username.Name
                , MembersInRolesTable.ApplicationName.Name
                , MembersInRolesTable.RoleName.Name)))
            {
                MembersInRolesTable.Username.AddParameter(cmd, username);
                MembersInRolesTable.ApplicationName.AddParameter(cmd, ApplicationName);

                using (IDataReader reader = ExecuteReader(cmd, "GetRolesForUser"))
                {
                    while (reader.Read())
                    {
                        rolesForUser.Add(reader.GetString(0));
                    }
                }
            }

            return rolesForUser.ToArray();
        }

        /// <summary>
        /// Gets the users in role.
        /// </summary>
        /// <param name="roleName">The name of the role to get the list of users for.</param>
        /// <returns>
        /// A string array containing the names of all the users who are members of the specified role for the configured applicationName.
        /// </returns>
        public override string[] GetUsersInRole(string roleName)
        {
            List<String> userNames = new List<string>();

            using (MySqlCommand cmd = new MySqlCommand(
                /* SELECT Username FROM MembersInRoles WHERE Rolename = ? AND ApplicationName = ? */
                String.Format(CultureInfo.InvariantCulture, "SELECT {3} FROM {0} WHERE {1} = @{1} AND {2} = @{2}"
                , MembersInRolesTable.TABLE_NAME
                , MembersInRolesTable.RoleName.Name
                , MembersInRolesTable.ApplicationName.Name
                , MembersInRolesTable.Username.Name)))
            {
                MembersInRolesTable.RoleName.AddParameter(cmd, roleName);
                MembersInRolesTable.ApplicationName.AddParameter(cmd, ApplicationName);

                using (IDataReader reader = ExecuteReader(cmd, "GetUsersInRole"))
                {
                    while (reader.Read())
                    {
                        userNames.Add(reader.GetString(0));
                    }
                }
            }

            return userNames.ToArray();
        }

        /// <summary>
        /// Determines whether [is user in role] [the specified username].
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="roleName">The role to search in.</param>
        /// <returns>
        /// 	<c>true</c> if [is user in role] [the specified username]; otherwise, <c>false</c>.
        /// </returns>
        public override bool IsUserInRole(string username, string roleName)
        {
            using (MySqlCommand cmd = new MySqlCommand(
                /* SELECT COUNT(*) FROM MembersInRoles WHERE Username = ? AND Rolename = ? AND ApplicationName = ? */
                String.Format(CultureInfo.InvariantCulture, "SELECT COUNT(*) FROM {0} WHERE {1} = @{1} AND {2} = @{2} AND {3} = @{3}"
                , MembersInRolesTable.TABLE_NAME
                , MembersInRolesTable.Username.Name
                , MembersInRolesTable.RoleName.Name
                , MembersInRolesTable.ApplicationName.Name)))
            {
                MembersInRolesTable.Username.AddParameter(cmd, username);
                MembersInRolesTable.RoleName.AddParameter(cmd, roleName);
                MembersInRolesTable.ApplicationName.AddParameter(cmd, ApplicationName);

                return Convert.ToInt32(ExecuteScalar(cmd, "IsUserInRole"), CultureInfo.InvariantCulture) > 0;
            }
        }

        /// <summary>
        /// Removes the users from roles.
        /// </summary>
        /// <param name="usernames">The usernames.</param>
        /// <param name="roleNames">A string array of role names to remove the specified user names from.</param>
        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            foreach (string rolename in roleNames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("Role name not found.");
                }
            }

            foreach (string username in usernames)
            {
                foreach (string rolename in roleNames)
                {
                    if (!IsUserInRole(username, rolename))
                    {
                        throw new ProviderException("User is not in role.");
                    }
                }
            }

            using (MySqlCommand cmd = new MySqlCommand(
                /* DELETE FROM MembersInRoles WHERE Username = ? AND Rolename = ? AND ApplicationName = ? */
                String.Format(CultureInfo.InvariantCulture, "DELETE FROM {0} WHERE {1} = @{1} AND {2} = @{2} AND {3} = @{3}"
                , MembersInRolesTable.TABLE_NAME
                , MembersInRolesTable.Username.Name
                , MembersInRolesTable.RoleName.Name
                , MembersInRolesTable.ApplicationName.Name)))
            {

                MySqlParameter userParam = MembersInRolesTable.Username.AddParameter(cmd, String.Empty);
                MySqlParameter roleParam = MembersInRolesTable.RoleName.AddParameter(cmd, String.Empty);
                MembersInRolesTable.ApplicationName.AddParameter(cmd, ApplicationName);

                foreach (string username in usernames)
                {
                    foreach (string rolename in roleNames)
                    {
                        userParam.Value = username;
                        roleParam.Value = rolename;
                        ExecuteNonQuery(cmd, "RemoveUsersFromRoles");
                    }
                }
            }
        }

        /// <summary>
        /// Roles the exists.
        /// </summary>
        /// <param name="roleName">The name of the role to search for in the data source.</param>
        /// <returns>
        /// true if the role name already exists in the data source for the configured applicationName; otherwise, false.
        /// </returns>
        public override bool RoleExists(string roleName)
        {
            using (MySqlCommand cmd = new MySqlCommand(
                /* SELECT COUNT(*) FROM MembershipRole WHERE Rolename = ? AND ApplicationName = ? */
                String.Format(CultureInfo.InvariantCulture, "SELECT COUNT(*) FROM {0} WHERE {1} = @{1} AND {2} = @{2}"
                , MembershipRoleTable.TABLE_NAME
                , MembershipRoleTable.RoleName.Name
                , MembershipRoleTable.ApplicationName.Name)))
            {
                MembershipRoleTable.RoleName.AddParameter(cmd, roleName);
                MembershipRoleTable.ApplicationName.AddParameter(cmd, ApplicationName);

                return Convert.ToInt32(ExecuteScalar(cmd, "RoleExists"), CultureInfo.InvariantCulture) > 0;
            }
        }

        /// <summary>
        /// Finds the users in role.
        /// </summary>
        /// <param name="roleName">The role to search in.</param>
        /// <param name="usernameToMatch">The username to match.</param>
        /// <returns>
        /// A string array containing the names of all the users where the user name matches <paramref name="usernameToMatch"/> and the user is a member of the specified role.
        /// </returns>
        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            List<String> usersInRole = new List<string>();

            using (MySqlCommand cmd = new MySqlCommand(
                /* SELECT Username FROM MembersInRoles  WHERE Username LIKE ? AND RoleName = ? AND ApplicationName = ? */
                String.Format(CultureInfo.InvariantCulture, "SELECT Username FROM {0}  WHERE {1} LIKE @{1} AND {2} = @{2} AND {3} = @{3}"
                , MembersInRolesTable.TABLE_NAME
                , MembersInRolesTable.Username.Name
                , MembersInRolesTable.RoleName.Name
                , MembersInRolesTable.ApplicationName.Name)))
            {
                MembersInRolesTable.Username.AddParameter(cmd, usernameToMatch);
                MembersInRolesTable.RoleName.AddParameter(cmd, roleName);
                MembersInRolesTable.ApplicationName.AddParameter(cmd, ApplicationName);

                using (IDataReader reader = ExecuteReader(cmd, "FindUsersInRole"))
                {
                    while (reader.Read())
                    {
                        usersInRole.Add(reader.GetString(0));
                    }
                }
            }

            return usersInRole.ToArray();
        }
        #endregion

        #region methods (private)
        private object ExecuteNonQuery(MySqlCommand command, string executeProcessName)
        {
            using (MySqlConnection connection = new MySqlConnection(connectionString))
            {
                command.Connection = connection;
                try
                {
                    connection.Open();

                    return command.ExecuteNonQuery();
                }
                catch (MySqlException e)
                {
                    if (WriteExceptionsToEventLog)
                    {
                        WriteToEventLog(e, executeProcessName);

                        throw new ProviderException(exceptionMessage);
                    }
                    else
                    {
                        throw;
                    }
                }
                finally
                {
                    connection.Close();
                }
            }
        }

        private object ExecuteScalar(MySqlCommand command, string executeProcessName)
        {
            using (MySqlConnection connection = new MySqlConnection(connectionString))
            {
                command.Connection = connection;
                try
                {
                    connection.Open();

                    return command.ExecuteScalar();
                }
                catch (MySqlException e)
                {
                    if (WriteExceptionsToEventLog)
                    {
                        WriteToEventLog(e, executeProcessName);

                        throw new ProviderException(exceptionMessage);
                    }
                    else
                    {
                        throw;
                    }
                }
                finally
                {
                    connection.Close();
                }
            }
        }

        private IDataReader ExecuteReader(MySqlCommand command, string executeProcessName)
        {
            MySqlConnection connection = new MySqlConnection(connectionString);
            command.Connection = connection;
            try
            {
                connection.Open();

                return command.ExecuteReader(CommandBehavior.CloseConnection);
            }
            catch (MySqlException e)
            {
                if (WriteExceptionsToEventLog)
                {
                    WriteToEventLog(e, executeProcessName);

                    throw new ProviderException(exceptionMessage);
                }
                else
                {
                    throw;
                }
            }
        }
        #endregion
    }
}
